#!/usr/bin/env python3  /*设定脚本文件直接能执行*/
# -*- coding:utf-8 -*-   /*设置编码*/
"""
@Author: zhuo
@Software: PyCharm
@File: tQSerialForm.py
@Time: 2024/1/15 13:22
@Function：ctQSerialForm：自定义串口设备连接界面类，可实现串口连接关闭、定时发送等功能。
"""
import re
import binascii
from datetime import datetime
from tDAL.tModbusHelper import *
from tUI.tQComboBox import ctQComboBox
from PySide6 import QtCore
from PySide6.QtWidgets import *
from PySide6.QtCore import Slot
from PySide6.QtGui import QBrush, QColor, QTextCursor

BLUE = "#1296DB"        # 蓝色
RED = "#BE431C"         # 红色
GEAY = "#F0F0F0"        # 灰色
NORMALGray = "#E1E1E1"  # Normal灰色


class MySignals(QObject):
    """
    自定义信号源
    """
    # 串口连接按键点击信号
    srlBtnClicked = QtCore.Signal(str)


# 实例化
GLOBAL_SRL_SIGNAL = MySignals()


class ctQSerialForm(QHBoxLayout):
    """
    自定义串口设备连接界面类，可实现串口连接关闭、定时发送等功能。
    """

    # 初始化
    def __init__(self, widget):
        # 继承自父类
        super(ctQSerialForm, self).__init__()
        # 所在的widget
        self.serialTab = widget
        # 调用设备连接界面布局函数
        self.__f_ConnectLayout()
        # modbus通讯类实例化
        self.rtu = ctModbusRTUHelper()
        # 定时器超时信号连接槽函数
        self.rtu.timer_send.timeout.connect(self.__f_uartSendTiming)
        # 串口接收到值后更新UI
        self.rtu.rwSignal.receive.connect(self.__f_uartReceiveDisplay)
        # 串口发送后更新UI
        self.rtu.rwSignal.write.connect(self.__f_sendUpdateUI)
        # 串口实例化
        self.srl = self.rtu.serial
        # 更新串口信息
        self.__f_cbbSerialRefresh()
        # 串口数据发送长度
        self.__m_sendLength = 0
        # 串口数据接收长度
        self.__m_receiveLength = 0

    # 设备连接界面布局函数
    def __f_ConnectLayout(self):
        """
        function:  设备连接界面布局函数
              in:  None
             out:  None
          return:  None
          others:  Connect Tab Layout
        """
        # 设备连接界面的总体布局
        self.mainConnectLayout = QHBoxLayout()
        # 设置总布局
        self.serialTab.setLayout(self.mainConnectLayout)
        # 设备连接界面左面的大框架
        self.leftLayout = QVBoxLayout()
        # 设备连接界面右面的大框架
        self.rightLayout = QVBoxLayout()

        # <editor-fold desc="标签的垂直Layout">
        self.lblSerial = QLabel()
        self.lblSerial.setText("串  口：")
        self.lblBaud = QLabel()
        self.lblBaud.setText("波特率：")
        self.lblParity = QLabel()
        self.lblParity.setText("校验位：")
        self.lblData = QLabel()
        self.lblData.setText("数据位：")
        self.lblStop = QLabel()
        self.lblStop.setText("停止位：")
        self.layoutLabel = QVBoxLayout()
        self.layoutLabel.addWidget(self.lblSerial)
        self.layoutLabel.addWidget(self.lblBaud)
        self.layoutLabel.addWidget(self.lblParity)
        self.layoutLabel.addWidget(self.lblData)
        self.layoutLabel.addWidget(self.lblStop)
        # </editor-fold>

        # <editor-fold desc="选择框的垂直Layout">
        self.cbbSerial = ctQComboBox()
        # 串口选择框clicked信号绑定的连接槽函数
        self.cbbSerial.clicked.connect(self.__f_cbbSerialRefresh)
        # 波特率选择框
        self.cbbBaud = QComboBox()
        # 校验位选择框
        self.cbbParity = QComboBox()
        # 数据位选择框
        self.cbbData = QComboBox()
        # 停止位选择框
        self.cbbStop = QComboBox()
        self.layoutCbb = QVBoxLayout()
        self.layoutCbb.addWidget(self.cbbSerial)
        self.layoutCbb.addWidget(self.cbbBaud)
        self.layoutCbb.addWidget(self.cbbParity)
        self.layoutCbb.addWidget(self.cbbData)
        self.layoutCbb.addWidget(self.cbbStop)
        self.cbbBaud.addItems(['100', '300', '600', '1200', '2400', '4800', '9600', '14400', '19200',
                               '38400', '56000', '57600', '115200', '128000', '256000'])
        self.cbbParity.addItems(['N', 'E', 'O'])
        self.cbbData.addItems(['5', '6', '7', '8'])
        self.cbbStop.addItems(['1', '1.5', '2'])
        # 波特率选择框设置默认选择值
        self.cbbBaud.setCurrentIndex(8)
        # 数据位选择框设置默认选择值
        self.cbbData.setCurrentIndex(3)
        # 校验位选择框设置默认选择值
        self.cbbParity.setCurrentIndex(0)
        # 停止位选择框设置默认选择值
        self.cbbStop.setCurrentIndex(0)
        # </editor-fold>

        # 标签和选择框的Layout
        self.layoutLblCbb = QHBoxLayout()
        # 标签和选择框的Layout 添加标签layout
        self.layoutLblCbb.addLayout(self.layoutLabel)
        # 标签和选择框的Layout 添加选择框layout
        self.layoutLblCbb.addLayout(self.layoutCbb)

        # <editor-fold desc="定时发送的layout 添加内容">
        self.ckbTimingSend = QCheckBox()
        self.ckbTimingSend.setText("定时发送")
        self.ckbTimingSend.stateChanged.connect(self.__f_uartTimerStateChanged)  # 定时发送状态为True，启动发送定时器
        self.textTime = QLineEdit("1000")
        # 文本框设置最小尺寸，不然会显示不全
        self.textTime.setMinimumSize(60, 10)
        self.textTime.textChanged.connect(self.__f_uartTimerStateChanged)  # 定时时间文本框内容改变，重新启动发送定时器
        self.lblMS = QLabel()
        self.lblMS.setText("ms")
        self.layoutTimingSend = QHBoxLayout()
        self.layoutTimingSend.addWidget(self.ckbTimingSend)
        self.layoutTimingSend.addWidget(self.textTime)
        self.layoutTimingSend.addWidget(self.lblMS)
        # </editor-fold>

        # <editor-fold desc="数据流 的layout布局">
        self.layoutDataFlow = QHBoxLayout()
        self.lbl1 = QLabel("数据流：累计发送：")
        self.lbl2 = QLabel(" 字节; 累积接收：")
        self.lbl3 = QLabel(" 字节")
        self.lblSend = QLabel("0")
        self.lblReceive = QLabel("0")
        # 水平Spacer
        self.spacerRefresh = QSpacerItem(1800, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.ckbIsRefresh = QCheckBox()
        self.ckbIsRefresh.setText("是否实时刷新发送数据")
        self.ckbIsRefresh.setChecked(True)
        self.btnClearRefresh = QPushButton("清空")
        self.btnClearRefresh.clicked.connect(self.__f_btnClearClicked)
        self.btnClearRefresh.setMinimumWidth(60)
        self.layoutDataFlow.addWidget(self.lbl1)
        self.layoutDataFlow.addWidget(self.lblSend)
        self.layoutDataFlow.addWidget(self.lbl2)
        self.layoutDataFlow.addWidget(self.lblReceive)
        self.layoutDataFlow.addWidget(self.lbl3)
        self.layoutDataFlow.addItem(self.spacerRefresh)
        self.layoutDataFlow.addWidget(self.btnClearRefresh)
        self.layoutDataFlow.addWidget(self.ckbIsRefresh)
        # </editor-fold>

        # <editor-fold desc="设备连接界面左面的大框架 添加内容">
        self.btnConnectSrl = QPushButton()
        self.btnConnectSrl.setText("打开(已关闭)")
        self.btnConnectSrl.clicked.connect(self.__f_btnConnectSrlClicked)
        # 状态选择框
        self.ckbIsShowSend = QCheckBox()
        # 状态选择框设置文本
        self.ckbIsShowSend.setText("显示发送")
        self.ckbIsShowSend.setChecked(True)
        # 状态选择框
        self.ckbIsDisplayTime = QCheckBox()
        self.ckbIsDisplayTime.setText("显示时间")
        # 状态选择框设置文本
        self.ckbIsDisplayTime.setChecked(True)
        self.btnSend = QPushButton()
        self.btnSend.setText("发送")
        self.btnSend.clicked.connect(self.__f_btnSendClicked)
        # 垂直Spacer
        self.spacerConnect = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.leftLayout.addLayout(self.layoutLblCbb)
        self.leftLayout.addWidget(self.btnConnectSrl)
        self.leftLayout.addItem(self.spacerConnect)
        self.leftLayout.addWidget(self.ckbIsDisplayTime)
        self.leftLayout.addWidget(self.ckbIsShowSend)
        self.leftLayout.addLayout(self.layoutTimingSend)
        self.leftLayout.addWidget(self.btnSend)
        # </editor-fold>

        # <editor-fold desc="设备连接界面右面的大框架 添加内容">
        self.textSerialReceive = QPlainTextEdit()
        # 数据接收框设置为只读
        self.textSerialReceive.setReadOnly(True)
        self.textSerialSend = QPlainTextEdit()
        # 数据接收框设置最大高度
        self.textSerialSend.setMaximumHeight(90)
        # 发送文本框设置合适尺寸，非固定非延伸
        self.textSerialSend.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.lblRight = QLabel("发送数据16进制;以空格分隔")
        self.spacerRS = QSpacerItem(1200, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.rightLayout.addLayout(self.layoutDataFlow)
        self.rightLayout.addWidget(self.textSerialReceive)
        self.rightLayout.addItem(self.spacerRS)
        self.rightLayout.addWidget(self.lblRight)
        self.rightLayout.addWidget(self.textSerialSend)
        # </editor-fold>
        # 设备连接界面 主框架 添加左面的大框架
        self.mainConnectLayout.addLayout(self.leftLayout)
        # 设备连接界面 主框架 添加右面的大框架
        self.mainConnectLayout.addLayout(self.rightLayout)

    # 串口信息发送按钮按下处理函数
    def __f_btnSendClicked(self):
        """
        function:  串口信息发送按钮按下处理函数
              in:  None
             out:  None
          return:  ir=True ok, ir=False some wrong
          others:  handle function of the button Send clicked
        """
        if self.rtu.f_readComIsOpen() == 2:
            sendText = self.textSerialSend.toPlainText().replace(" ", "")
            if len(sendText) % 2 == 0 and len(sendText) != 0:
                # 字符串转为bytes格式
                sendContent = sendText.encode("utf-8")
                # bytes转十六进制bytes
                sendContent = binascii.unhexlify(sendContent)
                # 获取当前时间
                now_time = datetime.now()
                # 设置时间输出格式
                new_time = now_time.strftime('[%Y-%m-%d-%H:%M:%S]')
                # 判断是否要实时显示发送接收内容
                if self.ckbIsRefresh.isChecked():
                    # 设置发送颜色
                    self.f_changeQPlainTextEditColor(self.textSerialReceive, RED)
                    pattern = re.compile('.{2}')
                    text = ' '.join(pattern.findall(sendText))
                    # 判断是否显示时间
                    if self.ckbIsDisplayTime.isChecked():
                        # 判断是否显示发送
                        if self.ckbIsShowSend.isChecked():
                            self.textSerialReceive.appendPlainText('TX' + f'{new_time} : ' + text)
                    else:
                        # 判断是否显示发送
                        if self.ckbIsShowSend.isChecked():
                            self.textSerialReceive.appendPlainText('TX' + ': ' + text)
                # 串口数据发送
                self.__m_sendLength += self.rtu.f_comSend(sendContent)
                # 累计发送字节label设置文本
                self.lblSend.setText(str(self.__m_sendLength))

    # 串口连接按钮按下处理函数
    def __f_btnConnectSrlClicked(self):
        """
        function:  串口连接按钮按下处理函数
              in:  None
             out:  ir
          return:  ir=True ok, ir=False some wrong
          others:  handle function of the button ConnectSrl clicked
        """
        ir = None
        # 判断是否选择了端口号
        if self.cbbSerial.currentText() != "":
            try:
                ir = self.rtu.f_openCom(self.cbbSerial.currentText(),
                                        self.cbbBaud.currentText(),
                                        self.cbbData.currentText(),
                                        self.cbbStop.currentText(),
                                        self.cbbParity.currentText())
            except Exception as e:
                QMessageBox.critical(self.serialTab, "警告", f"{self.srl.port}被占用,拒绝访问！")
            if ir:
                # 设置按钮的文本
                self.btnConnectSrl.setText("关闭(已打开)")
                # 将串口选择框状态设置为False
                self.cbbSerial.setEnabled(False)
                # 将波特率选择框状态设置为False
                self.cbbBaud.setEnabled(False)
                # 将数据位选择框状态设置为False
                self.cbbData.setEnabled(False)
                # 将停止位选择框状态设置为False
                self.cbbStop.setEnabled(False)
                # 将校验位选择框状态设置为False
                self.cbbParity.setEnabled(False)
                # 定时发送状态为True，启动发送定时器
                self.rtu.f_comSendTimer(self.ckbTimingSend.checkState(),
                                        self.textTime.text())
                GLOBAL_SRL_SIGNAL.srlBtnClicked.emit(f"{self.srl.port}已打开  波特率：{self.cbbBaud.currentText()}  "
                                                     f"数据位：{self.cbbData.currentText()}  "
                                                     f"停止位：{self.cbbStop.currentText()}  "
                                                     f"校验位：{self.cbbParity.currentText()}")
                return ir
            else:
                # 调用串口关闭函数
                self.rtu.f_closeCom()
                # 设置按钮的文本
                self.btnConnectSrl.setText("打开(已关闭)")
                # 将串口选择框状态设置为True
                self.cbbSerial.setEnabled(True)
                # 将波特率选择框状态设置为True
                self.cbbBaud.setEnabled(True)
                # 将数据位选择框状态设置为True
                self.cbbData.setEnabled(True)
                # 将停止位选择框状态设置为True
                self.cbbStop.setEnabled(True)
                # 将校验位选择框状态设置为True
                self.cbbParity.setEnabled(True)
                GLOBAL_SRL_SIGNAL.srlBtnClicked.emit(f"{self.srl.port}已关闭")
                return ir

    # 清空按钮按下处理函数
    def __f_btnClearClicked(self):
        """
        function:  清空按钮按下处理函数
              in:  None
             out:  None
          return:  None
          others:  Clear Button Pressed Handler Func
        """
        # 清空接收框文本
        self.textSerialReceive.clear()
        # 串口数据发送长度
        self.__m_sendLength = 0
        # 串口数据接收长度
        self.__m_receiveLength = 0
        # 累计接收字节label设置文本
        self.lblReceive.setText("0")
        self.lblSend.setText("0")

    # 串口数据发送后更新UI
    def __f_sendUpdateUI(self, send_data):
        """
        function:  串口数据发送后更新UI
              in:  send_data：write信号触发，发送出的信号
             out:  None
          return:  None
          others:  Serial Send Data Updated UI
        """
        # 获取当前时间
        now_time = datetime.now()
        # 设置时间输出格式
        new_time = now_time.strftime('[%Y-%m-%d-%H:%M:%S]')
        # 十六进制bytes转bytes
        send = binascii.hexlify(send_data)
        # bytes转字符串 转大写
        send = str(send, encoding="utf-8").upper()
        # 获取发送字节的长度，每发送一次，长度累加
        self.__m_sendLength += int(len(send) / 2)
        # 累计发送字节label设置文本
        self.lblSend.setText(str(self.__m_sendLength))
        # 判断是否要实时显示发送接收内容
        if self.ckbIsRefresh.isChecked():
            self.f_changeQPlainTextEditColor(self.textSerialReceive, RED)
            # 判断是否显示时间
            if self.ckbIsDisplayTime.isChecked():
                if self.ckbIsShowSend.isChecked():
                    # 向文本框内添加发送的字符
                    self.textSerialReceive.appendPlainText(
                        'TX' + f'{new_time} : ' + ' '.join(re.compile('.{2}').findall(send)))
            else:
                if self.ckbIsShowSend.isChecked():
                    # 向文本框内添加发送的字符
                    self.textSerialReceive.appendPlainText(
                        'TX' + ': ' + ' '.join(re.compile('.{2}').findall(send)))

    # 私有方法：串口发送定时器状态改变槽函数
    @Slot()
    def __f_uartTimerStateChanged(self):
        """
        function:  串口发送定时器状态改变槽函数
              in:  None
             out:  None
          return:  None
          others:  Serial Port Send Timer Status Changed Slot Function
        """
        if self.ckbTimingSend.isChecked():
            self.textTime.setEnabled(False)
        else:
            self.textTime.setEnabled(True)
        # 判断串口是否打开
        if self.rtu.f_readComIsOpen() == 2:
            # 定时发送状态为True，启动发送定时器
            self.rtu.f_comSendTimer(self.ckbTimingSend.checkState(), self.textTime.text())

    # 私有方法：串口数据定时发送槽函数
    @Slot()
    def __f_uartSendTiming(self):
        """
        function:  串口数据定时发送函数
              in:  isHex：是否十六进制显示
                   isDisplayTime：是否显示时间
             out:  None
          return:  None
          others:  Serial Port Data Sending Function
        """
        # 判断是否定时发送
        if self.ckbTimingSend.isChecked():
            # 判断串口是否打开
            if self.rtu.f_readComIsOpen() == 2:
                # 判断发送数据是否为空
                if self.textSerialSend.toPlainText() != "":
                    sendText = self.textSerialSend.toPlainText().replace(" ", "")
                    # 字符串转为bytes格式
                    sendContent = sendText.encode("utf-8")
                    try:
                        sendContent = binascii.unhexlify(sendContent)  # bytes转十六进制bytes
                    except Exception as e:
                        print(e)
                    # 获取当前时间
                    now_time = datetime.now()
                    # 设置时间输出格式
                    new_time = now_time.strftime('[%Y-%m-%d-%H:%M:%S]')
                    # 判断是否要实时显示发送接收内容
                    if self.ckbIsRefresh.isChecked():
                        # 设置发送颜色
                        self.f_changeQPlainTextEditColor(self.textSerialReceive, RED)
                        pattern = re.compile('.{2}')
                        # 显示的内容：每一个字节加一个空格
                        text = ' '.join(pattern.findall(sendText))
                        # 判断是否显示时间
                        if self.ckbIsDisplayTime.isChecked():
                            # 判断是否显示发送
                            if self.ckbIsShowSend.isChecked():
                                self.textSerialReceive.appendPlainText('TX' + f'{new_time} : ' + text)
                        else:
                            # 判断是否显示发送
                            if self.ckbIsShowSend.isChecked():
                                self.textSerialReceive.appendPlainText('TX' + ': ' + text)
                    # 发送字节长度累加
                    self.__m_sendLength += self.rtu.f_comSend(sendContent)
                    # 累计发送字节label设置文本
                    self.lblSend.setText(str(self.__m_sendLength))

    # 私有方法：串口数据接收显示槽函数
    @Slot()
    def __f_uartReceiveDisplay(self, receive_data):
        """
        function:  串口数据接收显示函数
              in:  isHex：是否十六进制显示
                   isDisplayTime：是否显示时间
             out:  None
          return:  None
          others:  Serial Port Data Receiving Display Function
        """
        # 获取当前时间
        now_time = datetime.now()
        # 设置时间输出格式
        new_time = now_time.strftime('[%Y-%m-%d-%H:%M:%S]')
        # 十六进制bytes转bytes
        receive = binascii.hexlify(receive_data)
        # bytes转字符串 转大写
        receive = str(receive, encoding="utf-8").upper()
        # 获取接收字节的长度，每接收一次，长度累加
        self.__m_receiveLength += int(len(receive) / 2)
        # 累计接收字节label设置文本
        self.lblReceive.setText(str(self.__m_receiveLength))
        # 判断是否要实时显示发送接收内容
        if self.ckbIsRefresh.isChecked():
            self.f_changeQPlainTextEditColor(self.textSerialReceive, BLUE)
            # 判断是否显示时间
            if self.ckbIsDisplayTime.isChecked():
                # 向文本框内添加接收的字符
                self.textSerialReceive.appendPlainText(
                    'RX' + f'{new_time} : ' + ' '.join(re.compile('.{2}').findall(receive)))
            else:
                # 向文本框内添加接收的字符
                self.textSerialReceive.appendPlainText(
                    'RX' + ': ' + ' '.join(re.compile('.{2}').findall(receive)))

    # 私有方法：串口选择框clicked信号绑定的连接槽函数，自定义的连接槽函数，传入的参数就是信号
    @Slot()
    def __f_cbbSerialRefresh(self):
        """
        function:  串口选择框clicked信号绑定的连接槽函数
              in:  None
             out:  None
          return:  None
          others:  The cbbSerial clicked connection slot function for signal binding
        """
        ir = self.rtu.f_refreshCom()
        # 先清空串口选择框，否则会重复添加
        self.cbbSerial.clear()
        if ir >= 1:
            self.cbbSerial.addItems(self.rtu.com_list)
            # 串口选择框设置默认选择值
            self.cbbSerial.setCurrentIndex(0)
        else:
            self.cbbSerial.clear()

    # QPlainTextEdit末尾增加文本时设置文本颜色
    @staticmethod
    def f_changeQPlainTextEditColor(plainTextEdit, color):
        """
        function:  QPlainTextEdit末尾增加文本时设置文本颜色
              in:  plainTextEdit：需要设置文本颜色的QPlainTextEdit控件
                   color：设置的颜色
             out:  None
          return:  None
          others:  QPlainTextEdit set the text color when adding text at the end
        """
        # 获取当前光标位置
        select_text = plainTextEdit.textCursor()
        # 创建TextCharFormat对象 获取当前字文本的字符串格式
        text_format = plainTextEdit.currentCharFormat()
        # 设置字体颜色
        text_format.setForeground(QBrush(QColor(color)))
        # 追加格式到原有文本
        select_text.mergeCharFormat(text_format)
        # 文本框设置颜色
        plainTextEdit.mergeCurrentCharFormat(text_format)